package com.evolve.autotest.compiler;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

import org.apache.log4j.Logger;

import com.evolve.autotest.Statement;
import com.evolve.autotest.Test;
import com.evolve.autotest.TestSuite;
import com.evolve.autotest.VerificationType;
import com.evolve.autotest.actions.ActionFactory;
import com.evolve.autotest.actions.IAction;
import com.evolve.autotest.util.FileUtil;

public class Compiler {

	private static final Logger logger = Logger.getLogger(Compiler.class.getName());

	private Test test;
	private TestSuite suite;
	private String token;
	private ActionFactory actionFactory;
	private boolean endStatementFound;

	private Statement statement;

	private Cursor cursor;

	/**
	 * Constructor.
	 */
	public Compiler() {
		endStatementFound = false;
	}

	/**
	 * Parses a file that contains test cases
	 * 
	 * @param testFile
	 * @return
	 */
	public TestSuite compile(File testFile, ActionFactory actionFactory) throws CompilerException {
		this.actionFactory = actionFactory;
		
		test = null;
		suite = new TestSuite(FileUtil.extractSimpleName(testFile));
		token = null;
		endStatementFound = false; 
		statement = null;
		cursor = null;
		
		State state = State.STATEMENT;

		FileInputStream fileStream = null;
		try {
			fileStream = new FileInputStream(testFile);

			cursor = new Cursor(fileStream);

			while (checkState(state) && (cursor.hasNextToken() || state == State.END)) {
				state = handleToken(cursor, state);
				logger.info(token);
			}
		} catch (FileNotFoundException e) {
			logger.error("Unable to start compiling test case file " + testFile.getAbsolutePath());
		} finally {
			if (fileStream != null) {
				try {
					fileStream.close();
				} catch (IOException e) {
					logger.error("Unable to close file stream when compiling " + testFile.getAbsolutePath());
				}
			}
		}

		// If the end statement was not found, throw an exception
		if (!endStatementFound) {
			throw new CompilerException(State.END_STATEMENT_NOT_FOUND, "The end of the test case was not found");
		}

		return suite;
	}

	/**
	 * Check if it is a OK state or not
	 * 
	 * @param state
	 * @return
	 * @throws CompilerException
	 */
	private boolean checkState(State state) throws CompilerException {
		if (state.ordinal() >= State.TEST_NAME_NOT_FOUND.ordinal()) {
			throw new CompilerException(state, "Compiler exception. " + state.toString() + " : " + token);
		}

		return true;
	}

	/**
	 * Determines which handler to use depending on the state and token
	 * 
	 * @param token
	 * @param state
	 */
	private State handleToken(Cursor cursor, State state) {

		if (state == State.START) {
			state = goToTestDeclaration(cursor);
		} else if (state == State.TEST_NAME) {
			state = goToTestName(cursor);
		} else if (state == State.STATEMENT) {
			state = goToStatement(cursor);
		} else if (state == State.VERIFY) {
			state = goToVerifyStatement(cursor);
		} else if (state == State.END) {
			state = goToEnd(cursor);
		}

		return state;
	}

	/**
	 * Handles the definition of a test case. Checks if token is "test"
	 * 
	 * @param token
	 *            The token to check
	 * @return Next state, faulty state if not found
	 */
	private State goToTestDeclaration(Cursor scanner) {
		// Create the test case
		test = new Test();

		endStatementFound = false;

		return State.TEST_NAME;
	}

	/**
	 * Checks that the test case name was set.
	 * 
	 * @param token
	 * @return
	 */
	private State goToTestName(Cursor cursor) {
		try {
			token = cursor.getNextToken(new char[] { '\r', '\n', '\t', ' ' }, new char[] { ':' });
		} catch (TokenNotFoundException e) {
			return State.TEST_NAME_NOT_FOUND;
		}

		if (token.length() == 0) {
			return State.TEST_NAME_NOT_FOUND;
		} else {

			test.setName(token);

			return State.STATEMENT;
		}
	}

	/**
	 * This state comes before the action name declaration. Like
	 * "verify:createFile"
	 * 
	 * @param cursor
	 */
	private State goToVerifyStatement(Cursor cursor) {
		statement = new Statement();
		statement.setVerification(VerificationType.VERIFY);
		test.addStatement(statement);
		
		return State.STATEMENT;
	}

	/**
	 * A statement can start with a method call or a "END" or "test". This
	 * method will then set the correct state.
	 * 
	 * @param token
	 *            The token to check
	 * @return
	 */
	private State goToStatement(Cursor cursor) {
		try {
			token = cursor.getNextToken(new char[] { ':', '\r', '\n', '\t', ' ' }, new char[] {});
		} catch (TokenNotFoundException e) {
			e.toString();
			return State.STATMENT_UNKNOWN;
		}

		if (token.equalsIgnoreCase("end")) {
			return State.END;
		} else if (cursor.getLastCharacter() == ':' && token.equalsIgnoreCase("test")) {
			return State.START;
		} else if (token.equalsIgnoreCase("verify")) {
			return State.VERIFY;
		} else if (actionFactory.containsAction(token)) {

			IAction action = actionFactory.getAction(token);

			if (statement == null) {
				statement = new Statement();
				test.addStatement(statement);
			}
			
			statement.setAction(action);

			return State.STATEMENT;
		} else {
			return State.STATMENT_UNKNOWN;
		}
	}

	/**
	 * This will close the test and add it to the list of compiled test cases
	 * 
	 * @param token
	 * @return
	 */
	private State goToEnd(Cursor scanner) {
		suite.addTest(test);

		endStatementFound = true;

		return State.STATEMENT;
	}
}
